{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Deep Research\n",
    "\n",
    "One of the classic cross-business Agentic use cases! This is huge."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<table style=\"margin: 0; text-align: left; width:100%\">\n",
    "    <tr>\n",
    "        <td style=\"width: 150px; height: 150px; vertical-align: middle;\">\n",
    "            <img src=\"../assets/business.png\" width=\"150\" height=\"150\" style=\"display: block;\" />\n",
    "        </td>\n",
    "        <td>\n",
    "            <h2 style=\"color:#00bfff;\">Commercial implications</h2>\n",
    "            <span style=\"color:#00bfff;\">A Deep Research agent is broadly applicable to any business area, and to your own day-to-day activities. You can make use of this yourself!\n",
    "            </span>\n",
    "        </td>\n",
    "    </tr>\n",
    "</table>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [],
   "source": [
    "from agents import Agent, WebSearchTool, trace, Runner, gen_trace_id, function_tool\n",
    "from agents.model_settings import ModelSettings\n",
    "from pydantic import BaseModel, Field\n",
    "from dotenv import load_dotenv\n",
    "import asyncio\n",
    "import sendgrid\n",
    "import os\n",
    "from sendgrid.helpers.mail import Mail, Email, To, Content\n",
    "from typing import Dict\n",
    "from IPython.display import display, Markdown"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "load_dotenv(override=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## OpenAI Hosted Tools\n",
    "\n",
    "OpenAI Agents SDK includes the following hosted tools:\n",
    "\n",
    "The `WebSearchTool` lets an agent search the web.  \n",
    "The `FileSearchTool` allows retrieving information from your OpenAI Vector Stores.  \n",
    "The `ComputerTool` allows automating computer use tasks like taking screenshots and clicking.\n",
    "\n",
    "### Important note - API charge of WebSearchTool\n",
    "\n",
    "This is costing me 2.5 cents per call for OpenAI WebSearchTool. That can add up to $2-$3 for the next 2 labs. We'll use free and low cost Search tools with other platforms, so feel free to skip running this if the cost is a concern. Also student Christian W. pointed out that OpenAI can sometimes charge for multiple searches for a single call, so it could sometimes cost more than 2.5 cents per call.\n",
    "\n",
    "Costs are here: https://platform.openai.com/docs/pricing#web-search"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [],
   "source": [
    "INSTRUCTIONS = \"You are a research assistant. Given a search term, you search the web for that term and \\\n",
    "produce a concise summary of the results. The summary must 2-3 paragraphs and less than 300 \\\n",
    "words. Capture the main points. Write succintly, no need to have complete sentences or good \\\n",
    "grammar. This will be consumed by someone synthesizing a report, so it's vital you capture the \\\n",
    "essence and ignore any fluff. Do not include any additional commentary other than the summary itself.\"\n",
    "\n",
    "search_agent = Agent(\n",
    "    name=\"Search agent\",\n",
    "    instructions=INSTRUCTIONS,\n",
    "    tools=[WebSearchTool(search_context_size=\"low\")],\n",
    "    model=\"gpt-4o-mini\",\n",
    "    model_settings=ModelSettings(tool_choice=\"required\"),\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "message = \"Latest AI Agent frameworks in 2025\"\n",
    "\n",
    "with trace(\"Search\"):\n",
    "    result = await Runner.run(search_agent, message)\n",
    "\n",
    "display(Markdown(result.final_output))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### As always, take a look at the trace\n",
    "\n",
    "https://platform.openai.com/traces"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### We will now use Structured Outputs, and include a description of the fields"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [],
   "source": [
    "# See note above about cost of WebSearchTool\n",
    "\n",
    "HOW_MANY_SEARCHES = 3\n",
    "\n",
    "INSTRUCTIONS = f\"You are a helpful research assistant. Given a query, come up with a set of web searches \\\n",
    "to perform to best answer the query. Output {HOW_MANY_SEARCHES} terms to query for.\"\n",
    "\n",
    "# Use Pydantic to define the Schema of our response - this is known as \"Structured Outputs\"\n",
    "# With massive thanks to student Wes C. for discovering and fixing a nasty bug with this!\n",
    "\n",
    "class WebSearchItem(BaseModel):\n",
    "    reason: str = Field(description=\"Your reasoning for why this search is important to the query.\")\n",
    "\n",
    "    query: str = Field(description=\"The search term to use for the web search.\")\n",
    "\n",
    "\n",
    "class WebSearchPlan(BaseModel):\n",
    "    searches: list[WebSearchItem] = Field(description=\"A list of web searches to perform to best answer the query.\")\n",
    "\n",
    "\n",
    "planner_agent = Agent(\n",
    "    name=\"PlannerAgent\",\n",
    "    instructions=INSTRUCTIONS,\n",
    "    model=\"gpt-4o-mini\",\n",
    "    output_type=WebSearchPlan,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "message = \"Latest AI Agent frameworks in 2025\"\n",
    "\n",
    "with trace(\"Search\"):\n",
    "    result = await Runner.run(planner_agent, message)\n",
    "    print(result.final_output)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "metadata": {},
   "outputs": [],
   "source": [
    "@function_tool\n",
    "def send_email(subject: str, html_body: str) -> Dict[str, str]:\n",
    "    \"\"\" Send out an email with the given subject and HTML body \"\"\"\n",
    "    sg = sendgrid.SendGridAPIClient(api_key=os.environ.get('SENDGRID_API_KEY'))\n",
    "    from_email = Email(\"ed@edwarddonner.com\") # Change this to your verified email\n",
    "    to_email = To(\"ed.donner@gmail.com\") # Change this to your email\n",
    "    content = Content(\"text/html\", html_body)\n",
    "    mail = Mail(from_email, to_email, subject, content).get()\n",
    "    response = sg.client.mail.send.post(request_body=mail)\n",
    "    return {\"status\": \"success\"}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "send_email"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {},
   "outputs": [],
   "source": [
    "INSTRUCTIONS = \"\"\"You are able to send a nicely formatted HTML email based on a detailed report.\n",
    "You will be provided with a detailed report. You should use your tool to send one email, providing the \n",
    "report converted into clean, well presented HTML with an appropriate subject line.\"\"\"\n",
    "\n",
    "email_agent = Agent(\n",
    "    name=\"Email agent\",\n",
    "    instructions=INSTRUCTIONS,\n",
    "    tools=[send_email],\n",
    "    model=\"gpt-4o-mini\",\n",
    ")\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "metadata": {},
   "outputs": [],
   "source": [
    "INSTRUCTIONS = (\n",
    "    \"You are a senior researcher tasked with writing a cohesive report for a research query. \"\n",
    "    \"You will be provided with the original query, and some initial research done by a research assistant.\\n\"\n",
    "    \"You should first come up with an outline for the report that describes the structure and \"\n",
    "    \"flow of the report. Then, generate the report and return that as your final output.\\n\"\n",
    "    \"The final output should be in markdown format, and it should be lengthy and detailed. Aim \"\n",
    "    \"for 5-10 pages of content, at least 1000 words.\"\n",
    ")\n",
    "\n",
    "\n",
    "class ReportData(BaseModel):\n",
    "    short_summary: str = Field(description=\"A short 2-3 sentence summary of the findings.\")\n",
    "\n",
    "    markdown_report: str = Field(description=\"The final report\")\n",
    "\n",
    "    follow_up_questions: list[str] = Field(description=\"Suggested topics to research further\")\n",
    "\n",
    "\n",
    "writer_agent = Agent(\n",
    "    name=\"WriterAgent\",\n",
    "    instructions=INSTRUCTIONS,\n",
    "    model=\"gpt-4o-mini\",\n",
    "    output_type=ReportData,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### The next 3 functions will plan and execute the search, using planner_agent and search_agent"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "metadata": {},
   "outputs": [],
   "source": [
    "async def plan_searches(query: str):\n",
    "    \"\"\" Use the planner_agent to plan which searches to run for the query \"\"\"\n",
    "    print(\"Planning searches...\")\n",
    "    result = await Runner.run(planner_agent, f\"Query: {query}\")\n",
    "    print(f\"Will perform {len(result.final_output.searches)} searches\")\n",
    "    return result.final_output\n",
    "\n",
    "async def perform_searches(search_plan: WebSearchPlan):\n",
    "    \"\"\" Call search() for each item in the search plan \"\"\"\n",
    "    print(\"Searching...\")\n",
    "    tasks = [asyncio.create_task(search(item)) for item in search_plan.searches]\n",
    "    results = await asyncio.gather(*tasks)\n",
    "    print(\"Finished searching\")\n",
    "    return results\n",
    "\n",
    "async def search(item: WebSearchItem):\n",
    "    \"\"\" Use the search agent to run a web search for each item in the search plan \"\"\"\n",
    "    input = f\"Search term: {item.query}\\nReason for searching: {item.reason}\"\n",
    "    result = await Runner.run(search_agent, input)\n",
    "    return result.final_output"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### The next 2 functions write a report and email it"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "metadata": {},
   "outputs": [],
   "source": [
    "async def write_report(query: str, search_results: list[str]):\n",
    "    \"\"\" Use the writer agent to write a report based on the search results\"\"\"\n",
    "    print(\"Thinking about report...\")\n",
    "    input = f\"Original query: {query}\\nSummarized search results: {search_results}\"\n",
    "    result = await Runner.run(writer_agent, input)\n",
    "    print(\"Finished writing report\")\n",
    "    return result.final_output\n",
    "\n",
    "async def send_email(report: ReportData):\n",
    "    \"\"\" Use the email agent to send an email with the report \"\"\"\n",
    "    print(\"Writing email...\")\n",
    "    result = await Runner.run(email_agent, report.markdown_report)\n",
    "    print(\"Email sent\")\n",
    "    return report"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Showtime!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "query =\"Latest AI Agent frameworks in 2025\"\n",
    "\n",
    "with trace(\"Research trace\"):\n",
    "    print(\"Starting research...\")\n",
    "    search_plan = await plan_searches(query)\n",
    "    search_results = await perform_searches(search_plan)\n",
    "    report = await write_report(query, search_results)\n",
    "    await send_email(report)  \n",
    "    print(\"Hooray!\")\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### As always, take a look at the trace\n",
    "\n",
    "https://platform.openai.com/traces"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<table style=\"margin: 0; text-align: left; width:100%\">\n",
    "    <tr>\n",
    "        <td style=\"width: 150px; height: 150px; vertical-align: middle;\">\n",
    "            <img src=\"../assets/thanks.png\" width=\"150\" height=\"150\" style=\"display: block;\" />\n",
    "        </td>\n",
    "        <td>\n",
    "            <h2 style=\"color:#00cc00;\">Congratulations on your progress, and a request</h2>\n",
    "            <span style=\"color:#00cc00;\">You've reached an important moment with the course; you've created a valuable Agent using one of the latest Agent frameworks. You've upskilled, and unlocked new commercial possibilities. Take a moment to celebrate your success!<br/><br/>Something I should ask you -- my editor would smack me if I didn't mention this. If you're able to rate the course on Udemy, I'd be seriously grateful: it's the most important way that Udemy decides whether to show the course to others and it makes a massive difference.<br/><br/>And another reminder to <a href=\"https://www.linkedin.com/in/eddonner/\">connect with me on LinkedIn</a> if you wish! If you wanted to post about your progress on the course, please tag me and I'll weigh in to increase your exposure.\n",
    "            </span>\n",
    "        </td>\n",
    "    </tr>"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
